library(qsmooth)
Setting options('download.file.method.GEOquery'='auto')
Setting options('GEOquery.inmemory.gpl'=FALSE)
library(Rtsne)
library(ggplot2)
library(formattable)
library(tidyverse)
Loading tidyverse: tibble
Loading tidyverse: tidyr
Loading tidyverse: readr
Loading tidyverse: purrr
Loading tidyverse: dplyr
Conflicts with tidy packages -------------------------------------------------------------------------------------------------------------------------------------------------------------------
filter(): dplyr, stats
lag(): dplyr, stats
source('~/git/scripts/theme_Publication.R')
# https://github.com/stephaniehicks/qsmooth
load('~/git/unified_gene_expression/data/lengthScaledTPM_eye_gtex.Rdata')
source('~/git/unified_gene_expression/scripts/parse_sample_attribute.R')
data.table 1.9.6 For help type ?data.table or https://github.com/Rdatatable/data.table/wiki
The fastest way to learn (by data.table authors): https://www.datacamp.com/courses/data-analysis-the-data-table-way
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
data.table + dplyr code now lives in dtplyr.
Please library(dtplyr)!
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Attaching package: ‘data.table’
The following objects are masked from ‘package:dplyr’:
between, last
The following object is masked from ‘package:purrr’:
transpose
Testing to see if running qsmooth (https://github.com/stephaniehicks/qsmooth, weighted quantile normalization) improves the clustering performance.
lengthScaledTPM <- lengthScaledTPM[,!(is.na(lengthScaledTPM[1,]))]
samples <- data.frame(colnames(lengthScaledTPM))
colnames(samples) <- 'sample_accession'
tissue_frame <- left_join(samples, core_info) %>% select(sample_accession, Tissue, Sub_Tissue) %>% distinct()
Joining, by = "sample_accession"
joining character vector and factor, coercing into character vector
# make sure they are lined up
tissues <- left_join(samples, tissue_frame)
Joining, by = "sample_accession"
joining character vector and factor, coercing into character vector
# just keep the keeper set of tissues (no prostrate, etc)
sub <- tissues %>% filter(Tissue %in% keepers)
lengthScaledTPM_sub<-lengthScaledTPM[,sub$sample_accession]
qs <- qsmooth(object = lengthScaledTPM_sub,groupFactor = as.factor(sub$Tissue))
lengthScaledTPM_qsmooth <- qsmoothData(qs)
set.seed(23235)
tsne_out <- Rtsne(as.matrix(log2(t(lengthScaledTPM_qsmooth)+1)),perplexity = 40, check_duplicates = FALSE, theta=0.0 )
tsne_plot <- data.frame(tsne_out$Y)
tsne_plot$sample_accession <- colnames(lengthScaledTPM_qsmooth)
tsne_plot %>% left_join(.,core_info) %>%
ggplot(.,aes(x=X1,y=X2,colour=Tissue,shape=Tissue)) +
geom_point(size=4) + scale_shape_manual(values=c(0:20,35:50)) +
ggtitle(paste0("t-sne. Perplexity = ", 40)) +
theme_Publication()
Joining, by = "sample_accession"

No qsmooth
set.seed(23235)
tsne_out <- Rtsne(as.matrix(log2(t(lengthScaledTPM_sub)+1)),perplexity = 40, check_duplicates = FALSE, theta=0.0 )
tsne_plot <- data.frame(tsne_out$Y)
tsne_plot$sample_accession <- colnames(lengthScaledTPM_sub)
tsne_plot %>% left_join(.,core_info) %>%
ggplot(.,aes(x=X1,y=X2,colour=Tissue,shape=Tissue)) +
geom_point(size=4) + scale_shape_manual(values=c(0:20,35:50)) +
ggtitle(paste0("t-sne. Perplexity = ", 40)) +
theme_Publication()
Joining, by = "sample_accession"

Expression profiles without qsmooth
gather_lST<-gather(lengthScaledTPM_sub,sample_accession) %>% left_join(.,core_tight)
Joining, by = "sample_accession"
ggplot(gather_lST,aes(x=log2(value+1),group=sample_accession))+geom_density()+facet_wrap(~Tissue)+coord_cartesian(ylim=c(0,0.5)) + theme_Publication()

Expression profiles with qsmooth. Odd spikes in the Cornea and RPE. Some weird patterns in number of lower-expressed genes (see Blood Vessel, Brain, Heart)
core_tight2 <- core_tight
core_tight2$sample_accession <- gsub(pattern='E-MTAB-',replacement = 'E.MTAB.',core_tight$sample_accession)
gather_lST<-gather(data.frame(lengthScaledTPM_qsmooth),sample_accession) %>% left_join(.,core_tight2)
Joining, by = "sample_accession"
ggplot(gather_lST,aes(x=log2(value+1),group=sample_accession))+geom_density()+facet_wrap(~Tissue)+coord_cartesian(ylim=c(0,0.5))+theme_Publication()

Let’s look at just the Cornea. Funky.
core_tight2 <- core_tight
core_tight2$sample_accession <- gsub(pattern='E-MTAB-',replacement = 'E.MTAB.',core_tight$sample_accession)
gather_lST <- gather(data.frame(lengthScaledTPM_qsmooth),sample_accession) %>% left_join(.,core_tight2) %>% filter(Tissue=='Cornea')
Joining, by = "sample_accession"
ggplot(gather_lST,aes(x=log2(value+1),group=sample_accession))+geom_density()+facet_wrap(~Tissue)+coord_cartesian(ylim=c(0,0.5)) + theme_Publication()

Let’s find the weird ones. SRS390607 to SRS390610. Weird median. Plotting the uncorrected values…Huh, seems like there are lots of zeros….
gather(data.frame(lengthScaledTPM_qsmooth),sample_accession) %>% left_join(.,core_tight2) %>% filter(Tissue=='Cornea') %>% group_by(sample_accession) %>% summarise(median(value)) %>% arrange(-`median(value)`) %>% head() %>% formattable()
Joining, by = "sample_accession"
core_eye_info %>% filter(sample_accession=='SRS390607') %>% formattable()
gather_lST<-gather(lengthScaledTPM_sub,sample_accession) %>% left_join(.,core_tight) %>% filter(sample_accession %in% c('SRS390607','SRS390608','SRS390609','SRS390610'))
Joining, by = "sample_accession"
ggplot(gather_lST,aes(x=log2(value+1)))+geom_density()+facet_wrap(~Tissue)+coord_cartesian(ylim=c(0,0.5)) + theme_Publication()

Let’s look at the number level for one of those odd samples. Uh, yikes. Let’s check the median for ALL of the samples to see if any others have this problem. I’m guessing more do.
summary(lengthScaledTPM_sub[,'SRS390607'])
Min. 1st Qu. Median Mean 3rd Qu. Max.
0 0 0 320 0 6503000
Yeah, a few. A plateau at a median of 50.
apply(lengthScaledTPM,2,function(x) median(x)) %>% sort() %>% head(60) %>% data.frame() %>% formattable()
apply(lengthScaledTPM,2,function(x) median(x)) %>% density() %>% plot()
axis(side = 1, at=seq(0,500,10))

Let’s redo the qsmooth’ed density plots above, but with the low (<10) colored. Bam, that’s it.
median_TPM <- apply(lengthScaledTPM,2,function(x) median(x)) %>% sort() %>% formattable()
lows <- names(median_TPM[median_TPM<10])
core_tight2 <- core_tight
core_tight2$sample_accession <- gsub(pattern='E-MTAB-',replacement = 'E.MTAB.',core_tight$sample_accession)
gather_lST<-gather(data.frame(lengthScaledTPM_qsmooth),sample_accession) %>% left_join(.,core_tight2) %>% mutate(LowMedian=ifelse(sample_accession %in% lows, 'LowMedian','OK'))
Joining, by = "sample_accession"
ggplot(gather_lST,aes(x=log2(value+1),group=sample_accession,colour=LowMedian))+geom_density(alpha=0.5)+facet_wrap(~Tissue)+coord_cartesian(ylim=c(0,0.5))+theme_Publication()

Let’s up the median threshold to, say, 50. This is probably a reasonable place to set it.
median_TPM <- apply(lengthScaledTPM,2,function(x) median(x)) %>% sort() %>% formattable()
lows <- names(median_TPM[median_TPM<60])
core_tight2 <- core_tight
core_tight2$sample_accession <- gsub(pattern='E-MTAB-',replacement = 'E.MTAB.',core_tight$sample_accession)
gather_lST<-gather(data.frame(lengthScaledTPM_qsmooth),sample_accession) %>% left_join(.,core_tight2) %>% mutate(LowMedian=ifelse(sample_accession %in% lows, 'LowMedian','OK'))
Joining, by = "sample_accession"
ggplot(gather_lST,aes(x=log2(value+1),group=sample_accession,colour=LowMedian))+geom_density(alpha=0.5)+facet_wrap(~Tissue)+coord_cartesian(ylim=c(0,0.5))+theme_Publication()

Ugh, this tosses a LOT of my fetal RPE (44 originally). Eh, life.
gather_lST %>% filter(sample_accession %in% lows) %>% select(sample_accession, Tissue, Sub_Tissue) %>% distinct() %>% group_by(Tissue, Sub_Tissue) %>% summarise(Count=n()) %>% formattable()
Redo the tsne (qsmooth’ed) and tossing the above samples. Looks better. A couple of Retina ($) samples that have wandered away (X2=10,X1=-10).
set.seed(23235)
lengthScaledTPM_qsmooth_highExp <- as.matrix(lengthScaledTPM_qsmooth[,!colnames(lengthScaledTPM_qsmooth) %in% lows])
tsne_out <- Rtsne(as.matrix(log2(t(lengthScaledTPM_qsmooth_highExp)+1)),perplexity = 50, check_duplicates = FALSE, theta=0.0 )
tsne_plot <- data.frame(tsne_out$Y)
tsne_plot$sample_accession <- colnames(lengthScaledTPM_qsmooth_highExp)
tsne_plot %>% left_join(.,core_info) %>%
ggplot(.,aes(x=X1,y=X2,colour=Tissue,shape=Tissue)) +
geom_point(size=4) + scale_shape_manual(values=c(0:20,35:50)) +
ggtitle(paste0("t-sne. Perplexity = ", 50)) +
theme_Publication()
Joining, by = "sample_accession"

Let’s do a quick check on genes with overall low experession. Less than 1 count on average across all samples, for a gene.
density(rowSums(log2(lengthScaledTPM+1))) %>% plot()

table((rowSums(lengthScaledTPM)/ncol(lengthScaledTPM))<1)
FALSE TRUE
19277 1053
lowly_expressed_genes <- row.names(lengthScaledTPM[(rowSums(lengthScaledTPM)/ncol(lengthScaledTPM)<1),])
OK, let’s also toss these and see how that changes the t-sne clustering.
set.seed(23235)
lengthScaledTPM_qsmooth_highExp <- as.matrix(lengthScaledTPM_qsmooth[,!colnames(lengthScaledTPM_qsmooth) %in% lows])
lengthScaledTPM_qsmooth_highExp_remove_lowGenes <- lengthScaledTPM_qsmooth_highExp[!(row.names(lengthScaledTPM_qsmooth_highExp) %in% lowly_expressed_genes),]
tsne_out <- Rtsne(as.matrix(log2(t(lengthScaledTPM_qsmooth_highExp_remove_lowGenes)+1)),perplexity = 50, check_duplicates = FALSE, theta=0.0 )
tsne_plot <- data.frame(tsne_out$Y)
tsne_plot$sample_accession <- colnames(lengthScaledTPM_qsmooth_highExp_remove_lowGenes)
tsne_plot %>% left_join(.,core_info) %>%
ggplot(.,aes(x=X1,y=X2,colour=Tissue,shape=Tissue)) +
geom_point(size=4) + scale_shape_manual(values=c(0:20,35:50)) +
ggtitle(paste0("t-sne. Perplexity = ", 50)) +
theme_Publication()
Joining, by = "sample_accession"

OK, this is getting hard/impossible to see whether I’m doing any better. Need to get more analytical. Metric: cluster purity. K-means cluster (k number is number of sub-tissues or tissues?), check purity of each cluster.
load('~/git/unified_gene_expression/data/lengthScaledTPM_eye_gtex.Rdata')
lengthScaledTPM <- lengthScaledTPM[,!(is.na(lengthScaledTPM[1,]))]
sub <- tissues %>% filter(Tissue %in% keepers)
lengthScaledTPM_sub<-lengthScaledTPM[,sub$sample_accession]
set.seed(23235)
tsne_out <- Rtsne(as.matrix(log2(t(lengthScaledTPM_sub)+1)),perplexity = 40, check_duplicates = FALSE, theta=0.0 )
tsne_plot <- data.frame(tsne_out$Y)
tsne_plot$sample_accession <- colnames(lengthScaledTPM_sub)
tsne_go <- tsne_plot %>% left_join(.,core_info)
Joining, by = "sample_accession"
# Determine number of clusters
wss <- (nrow(tsne_go[,c('X1','X2')])-1)*sum(apply(tsne_go[,c('X1','X2')],2,var))
for (i in 2:60) wss[i] <- sum(kmeans(tsne_go[,c('X1','X2')],
centers=i)$withinss)
plot(1:60, wss, type="b", xlab="Number of Clusters",
ylab="Within groups sum of squares")

# 24 is the total number of Tissues
# 48 is the total number of Sub Tissues
set.seed(23235)
fit <- kmeans(tsne_go[,c('X1','X2')], 24)
cbind(tsne_go, fit$cluster) %>% group_by(fit$cluster,Tissue) %>% summarise(Count = n()) %>% mutate(freq = Count /sum(Count)) %>% formattable()
Now go deeper and count number of Tissue types and purity (highest freq) for each cluster
cbind(tsne_go, fit$cluster) %>% group_by(fit$cluster,Tissue) %>% summarise(Count = n()) %>% mutate(freq = Count /sum(Count)) %>% summarise(Tissue_Count = n(), highest_freq=max(freq)) %>% formattable()
OK, now take the mean of Tissue_Count and highest_freq, for two metrics for each tnse.
cbind(tsne_go, fit$cluster) %>% group_by(fit$cluster,Tissue) %>% summarise(Count = n()) %>% mutate(freq = Count /sum(Count)) %>% summarise(Tissue_Count = n(), highest_freq=max(freq)) %>% mutate(mean(Tissue_Count), mean(highest_freq)) %>% select(`mean(Tissue_Count)`, `mean(highest_freq)`) %>% distinct()
Cool. Let’s go the end (qsmooth, removal of low-median samples, removal of low expression genes) and see if these metrics get better. YES THEY DO. IT WORKS. OMG. (Sorry, usually stuff like this blows up in my face once I test it rigorously).
set.seed(23235)
tsne_out <- Rtsne(as.matrix(log2(t(lengthScaledTPM_qsmooth_highExp_remove_lowGenes)+1)),perplexity = 40, check_duplicates = FALSE, theta=0.0 )
tsne_plot <- data.frame(tsne_out$Y)
tsne_plot$sample_accession <- colnames(lengthScaledTPM_qsmooth_highExp_remove_lowGenes)
tsne_go <- tsne_plot %>% left_join(.,core_info)
Joining, by = "sample_accession"
set.seed(23235)
fit <- kmeans(tsne_go[,c('X1','X2')], 24)
cbind(tsne_go, fit$cluster) %>% group_by(fit$cluster,Tissue) %>% summarise(Count = n()) %>% mutate(freq = Count /sum(Count)) %>% summarise(Tissue_Count = n(), highest_freq=max(freq)) %>% mutate(mean(Tissue_Count), mean(highest_freq)) %>% select(`mean(Tissue_Count)`, `mean(highest_freq)`) %>% distinct()
Let’s see whether having qsmooth correct for Sub Tissues improves performance.
set.seed(23235)
tsne_out <- Rtsne(as.matrix(log2(t(lengthScaledTPM_qsmooth_subTissue)+1)),perplexity = 40, check_duplicates = FALSE, theta=0.0 )
tsne_plot <- data.frame(tsne_out$Y)
tsne_plot$sample_accession <- colnames(lengthScaledTPM_qsmooth_subTissue)
tsne_go <- tsne_plot %>% left_join(.,core_info)
Joining, by = "sample_accession"
set.seed(23235)
fit <- kmeans(tsne_go[,c('X1','X2')], 24)
cbind(tsne_go, fit$cluster) %>% group_by(fit$cluster,Tissue) %>% summarise(Count = n()) %>% mutate(freq = Count /sum(Count)) %>% summarise(Tissue_Count = n(), highest_freq=max(freq)) %>% mutate(mean(Tissue_Count), mean(highest_freq)) %>% select(`mean(Tissue_Count)`, `mean(highest_freq)`) %>% distinct()
Now with the corrections….kind of middling. May not be enough tissues per group.
set.seed(23235)
tsne_out <- Rtsne(as.matrix(log2(t(lengthScaledTPM_qsmooth_highExp_remove_lowGenes_subTissue)+1)),perplexity = 40, check_duplicates = FALSE, theta=0.0 )
tsne_plot <- data.frame(tsne_out$Y)
tsne_plot$sample_accession <- colnames(lengthScaledTPM_qsmooth_highExp_remove_lowGenes_subTissue)
tsne_go <- tsne_plot %>% left_join(.,core_info)
Joining, by = "sample_accession"
set.seed(23235)
fit <- kmeans(tsne_go[,c('X1','X2')], 24)
cbind(tsne_go, fit$cluster) %>% group_by(fit$cluster,Tissue) %>% summarise(Count = n()) %>% mutate(freq = Count /sum(Count)) %>% summarise(Tissue_Count = n(), highest_freq=max(freq)) %>% mutate(mean(Tissue_Count), mean(highest_freq)) %>% select(`mean(Tissue_Count)`, `mean(highest_freq)`) %>% distinct()
OK, saving lengthScaledTPM_qsmooth_highExp_remove_lowGenes for downstream use:
save(lengthScaledTPM_qsmooth_highExp_remove_lowGenes, file='~/git/unified_gene_expression/data/lengthScaledTPM_processed.Rdata')
LS0tCnRpdGxlOiAicXNtb290aCB0ZXN0IgpvdXRwdXQ6IGh0bWxfbm90ZWJvb2sKLS0tCmBgYHtyfQpsaWJyYXJ5KHFzbW9vdGgpCmxpYnJhcnkoUnRzbmUpCmxpYnJhcnkoZ2dwbG90MikKbGlicmFyeShmb3JtYXR0YWJsZSkKbGlicmFyeSh0aWR5dmVyc2UpCnNvdXJjZSgnfi9naXQvc2NyaXB0cy90aGVtZV9QdWJsaWNhdGlvbi5SJykKIyBodHRwczovL2dpdGh1Yi5jb20vc3RlcGhhbmllaGlja3MvcXNtb290aApsb2FkKCd+L2dpdC91bmlmaWVkX2dlbmVfZXhwcmVzc2lvbi9kYXRhL2xlbmd0aFNjYWxlZFRQTV9leWVfZ3RleC5SZGF0YScpCnNvdXJjZSgnfi9naXQvdW5pZmllZF9nZW5lX2V4cHJlc3Npb24vc2NyaXB0cy9wYXJzZV9zYW1wbGVfYXR0cmlidXRlLlInKQpgYGAKVGVzdGluZyB0byBzZWUgaWYgcnVubmluZyBxc21vb3RoIChodHRwczovL2dpdGh1Yi5jb20vc3RlcGhhbmllaGlja3MvcXNtb290aCwgd2VpZ2h0ZWQgcXVhbnRpbGUgbm9ybWFsaXphdGlvbikgaW1wcm92ZXMgdGhlIGNsdXN0ZXJpbmcgcGVyZm9ybWFuY2UuCgpgYGB7cixmaWcud2lkdGg9NCwgZmlnLmhlaWdodD00LjV9Cmxlbmd0aFNjYWxlZFRQTSA8LSBsZW5ndGhTY2FsZWRUUE1bLCEoaXMubmEobGVuZ3RoU2NhbGVkVFBNWzEsXSkpXQoKc2FtcGxlcyA8LSBkYXRhLmZyYW1lKGNvbG5hbWVzKGxlbmd0aFNjYWxlZFRQTSkpCmNvbG5hbWVzKHNhbXBsZXMpIDwtICdzYW1wbGVfYWNjZXNzaW9uJwp0aXNzdWVfZnJhbWUgPC0gbGVmdF9qb2luKHNhbXBsZXMsIGNvcmVfaW5mbykgJT4lIHNlbGVjdChzYW1wbGVfYWNjZXNzaW9uLCBUaXNzdWUsIFN1Yl9UaXNzdWUpICU+JSBkaXN0aW5jdCgpCiMgbWFrZSBzdXJlIHRoZXkgYXJlIGxpbmVkIHVwCnRpc3N1ZXMgPC0gbGVmdF9qb2luKHNhbXBsZXMsIHRpc3N1ZV9mcmFtZSkKIyBqdXN0IGtlZXAgdGhlIGtlZXBlciBzZXQgb2YgdGlzc3VlcyAobm8gcHJvc3RyYXRlLCBldGMpCnN1YiA8LSB0aXNzdWVzICU+JSBmaWx0ZXIoVGlzc3VlICVpbiUga2VlcGVycykKbGVuZ3RoU2NhbGVkVFBNX3N1YjwtbGVuZ3RoU2NhbGVkVFBNWyxzdWIkc2FtcGxlX2FjY2Vzc2lvbl0KcXMgPC0gcXNtb290aChvYmplY3QgPSBsZW5ndGhTY2FsZWRUUE1fc3ViLGdyb3VwRmFjdG9yID0gYXMuZmFjdG9yKHN1YiRUaXNzdWUpKQpsZW5ndGhTY2FsZWRUUE1fcXNtb290aCA8LSBxc21vb3RoRGF0YShxcykKCnNldC5zZWVkKDIzMjM1KQp0c25lX291dCA8LSBSdHNuZShhcy5tYXRyaXgobG9nMih0KGxlbmd0aFNjYWxlZFRQTV9xc21vb3RoKSsxKSkscGVycGxleGl0eSA9IDQwLCBjaGVja19kdXBsaWNhdGVzID0gRkFMU0UsIHRoZXRhPTAuMCApCnRzbmVfcGxvdCA8LSBkYXRhLmZyYW1lKHRzbmVfb3V0JFkpCnRzbmVfcGxvdCRzYW1wbGVfYWNjZXNzaW9uIDwtIGNvbG5hbWVzKGxlbmd0aFNjYWxlZFRQTV9xc21vb3RoKQoKdHNuZV9wbG90ICU+JSBsZWZ0X2pvaW4oLixjb3JlX2luZm8pICAlPiUKICBnZ3Bsb3QoLixhZXMoeD1YMSx5PVgyLGNvbG91cj1UaXNzdWUsc2hhcGU9VGlzc3VlKSkgKyAKICBnZW9tX3BvaW50KHNpemU9NCkgKyBzY2FsZV9zaGFwZV9tYW51YWwodmFsdWVzPWMoMDoyMCwzNTo1MCkpICsKICBnZ3RpdGxlKHBhc3RlMCgidC1zbmUuIFBlcnBsZXhpdHkgPSAiLCA0MCkpICsKICB0aGVtZV9QdWJsaWNhdGlvbigpCmBgYAoKTm8gcXNtb290aApgYGB7cixmaWcud2lkdGg9NCwgZmlnLmhlaWdodD00LjV9CnNldC5zZWVkKDIzMjM1KQp0c25lX291dCA8LSBSdHNuZShhcy5tYXRyaXgobG9nMih0KGxlbmd0aFNjYWxlZFRQTV9zdWIpKzEpKSxwZXJwbGV4aXR5ID0gNDAsIGNoZWNrX2R1cGxpY2F0ZXMgPSBGQUxTRSwgdGhldGE9MC4wICkKCnRzbmVfcGxvdCA8LSBkYXRhLmZyYW1lKHRzbmVfb3V0JFkpCnRzbmVfcGxvdCRzYW1wbGVfYWNjZXNzaW9uIDwtIGNvbG5hbWVzKGxlbmd0aFNjYWxlZFRQTV9zdWIpCgp0c25lX3Bsb3QgJT4lIGxlZnRfam9pbiguLGNvcmVfaW5mbykgICU+JQogIGdncGxvdCguLGFlcyh4PVgxLHk9WDIsY29sb3VyPVRpc3N1ZSxzaGFwZT1UaXNzdWUpKSArIAogIGdlb21fcG9pbnQoc2l6ZT00KSArIHNjYWxlX3NoYXBlX21hbnVhbCh2YWx1ZXM9YygwOjIwLDM1OjUwKSkgKwogIGdndGl0bGUocGFzdGUwKCJ0LXNuZS4gUGVycGxleGl0eSA9ICIsIDQwKSkgKwogIHRoZW1lX1B1YmxpY2F0aW9uKCkKYGBgCgpFeHByZXNzaW9uIHByb2ZpbGVzIHdpdGhvdXQgcXNtb290aApgYGB7cixmaWcud2lkdGg9NCwgZmlnLmhlaWdodD01fQpnYXRoZXJfbFNUPC1nYXRoZXIobGVuZ3RoU2NhbGVkVFBNX3N1YixzYW1wbGVfYWNjZXNzaW9uKSAlPiUgbGVmdF9qb2luKC4sY29yZV90aWdodCkKZ2dwbG90KGdhdGhlcl9sU1QsYWVzKHg9bG9nMih2YWx1ZSsxKSxncm91cD1zYW1wbGVfYWNjZXNzaW9uKSkrZ2VvbV9kZW5zaXR5KCkrZmFjZXRfd3JhcCh+VGlzc3VlKStjb29yZF9jYXJ0ZXNpYW4oeWxpbT1jKDAsMC41KSkgKyB0aGVtZV9QdWJsaWNhdGlvbigpCmBgYAoKRXhwcmVzc2lvbiBwcm9maWxlcyB3aXRoIHFzbW9vdGguIE9kZCBzcGlrZXMgaW4gdGhlIENvcm5lYSBhbmQgUlBFLiBTb21lIHdlaXJkIHBhdHRlcm5zIGluIG51bWJlciBvZiBsb3dlci1leHByZXNzZWQgZ2VuZXMgKHNlZSBCbG9vZCBWZXNzZWwsIEJyYWluLCBIZWFydCkKYGBge3IsZmlnLndpZHRoPTYsIGZpZy5oZWlnaHQ9N30KY29yZV90aWdodDIgPC0gY29yZV90aWdodApjb3JlX3RpZ2h0MiRzYW1wbGVfYWNjZXNzaW9uIDwtIGdzdWIocGF0dGVybj0nRS1NVEFCLScscmVwbGFjZW1lbnQgPSAnRS5NVEFCLicsY29yZV90aWdodCRzYW1wbGVfYWNjZXNzaW9uKQpnYXRoZXJfbFNUPC1nYXRoZXIoZGF0YS5mcmFtZShsZW5ndGhTY2FsZWRUUE1fcXNtb290aCksc2FtcGxlX2FjY2Vzc2lvbikgJT4lIGxlZnRfam9pbiguLGNvcmVfdGlnaHQyKQpnZ3Bsb3QoZ2F0aGVyX2xTVCxhZXMoeD1sb2cyKHZhbHVlKzEpLGdyb3VwPXNhbXBsZV9hY2Nlc3Npb24pKStnZW9tX2RlbnNpdHkoKStmYWNldF93cmFwKH5UaXNzdWUpK2Nvb3JkX2NhcnRlc2lhbih5bGltPWMoMCwwLjUpKSt0aGVtZV9QdWJsaWNhdGlvbigpCmBgYAoKTGV0J3MgbG9vayBhdCBqdXN0IHRoZSBDb3JuZWEuIEZ1bmt5LiAKYGBge3IsZmlnLndpZHRoPTIsIGZpZy5oZWlnaHQ9MS41fQpjb3JlX3RpZ2h0MiA8LSBjb3JlX3RpZ2h0CmNvcmVfdGlnaHQyJHNhbXBsZV9hY2Nlc3Npb24gPC0gZ3N1YihwYXR0ZXJuPSdFLU1UQUItJyxyZXBsYWNlbWVudCA9ICdFLk1UQUIuJyxjb3JlX3RpZ2h0JHNhbXBsZV9hY2Nlc3Npb24pCmdhdGhlcl9sU1QgPC0gZ2F0aGVyKGRhdGEuZnJhbWUobGVuZ3RoU2NhbGVkVFBNX3FzbW9vdGgpLHNhbXBsZV9hY2Nlc3Npb24pICU+JSBsZWZ0X2pvaW4oLixjb3JlX3RpZ2h0MikgJT4lIGZpbHRlcihUaXNzdWU9PSdDb3JuZWEnKQpnZ3Bsb3QoZ2F0aGVyX2xTVCxhZXMoeD1sb2cyKHZhbHVlKzEpLGdyb3VwPXNhbXBsZV9hY2Nlc3Npb24pKStnZW9tX2RlbnNpdHkoKStmYWNldF93cmFwKH5UaXNzdWUpK2Nvb3JkX2NhcnRlc2lhbih5bGltPWMoMCwwLjUpKSArIHRoZW1lX1B1YmxpY2F0aW9uKCkKYGBgCkxldCdzIGZpbmQgdGhlIHdlaXJkIG9uZXMuIFNSUzM5MDYwNyB0byBTUlMzOTA2MTAuIFdlaXJkIG1lZGlhbi4gUGxvdHRpbmcgdGhlIHVuY29ycmVjdGVkIHZhbHVlcy4uLkh1aCwgc2VlbXMgbGlrZSB0aGVyZSBhcmUgbG90cyBvZiB6ZXJvcy4uLi4KYGBge3IsZmlnLndpZHRoPTIsIGZpZy5oZWlnaHQ9Mi41fQpnYXRoZXIoZGF0YS5mcmFtZShsZW5ndGhTY2FsZWRUUE1fcXNtb290aCksc2FtcGxlX2FjY2Vzc2lvbikgJT4lIGxlZnRfam9pbiguLGNvcmVfdGlnaHQyKSAlPiUgZmlsdGVyKFRpc3N1ZT09J0Nvcm5lYScpICU+JSBncm91cF9ieShzYW1wbGVfYWNjZXNzaW9uKSAlPiUgc3VtbWFyaXNlKG1lZGlhbih2YWx1ZSkpICU+JSBhcnJhbmdlKC1gbWVkaWFuKHZhbHVlKWApICU+JSBoZWFkKCkgJT4lIGZvcm1hdHRhYmxlKCkKCmNvcmVfZXllX2luZm8gJT4lIGZpbHRlcihzYW1wbGVfYWNjZXNzaW9uPT0nU1JTMzkwNjA3JykgJT4lIGZvcm1hdHRhYmxlKCkKCmdhdGhlcl9sU1Q8LWdhdGhlcihsZW5ndGhTY2FsZWRUUE1fc3ViLHNhbXBsZV9hY2Nlc3Npb24pICU+JSBsZWZ0X2pvaW4oLixjb3JlX3RpZ2h0KSAlPiUgZmlsdGVyKHNhbXBsZV9hY2Nlc3Npb24gJWluJSBjKCdTUlMzOTA2MDcnLCdTUlMzOTA2MDgnLCdTUlMzOTA2MDknLCdTUlMzOTA2MTAnKSkKZ2dwbG90KGdhdGhlcl9sU1QsYWVzKHg9bG9nMih2YWx1ZSsxKSkpK2dlb21fZGVuc2l0eSgpK2ZhY2V0X3dyYXAoflRpc3N1ZSkrY29vcmRfY2FydGVzaWFuKHlsaW09YygwLDAuNSkpICsgdGhlbWVfUHVibGljYXRpb24oKQoKYGBgCkxldCdzIGxvb2sgYXQgdGhlIG51bWJlciBsZXZlbCBmb3Igb25lIG9mIHRob3NlIG9kZCBzYW1wbGVzLiBVaCwgeWlrZXMuIExldCdzIGNoZWNrIHRoZSBtZWRpYW4gZm9yIEFMTCBvZiB0aGUgc2FtcGxlcyB0byBzZWUgaWYgYW55IG90aGVycyBoYXZlIHRoaXMgcHJvYmxlbS4gSSdtIGd1ZXNzaW5nIG1vcmUgZG8uIApgYGB7cn0Kc3VtbWFyeShsZW5ndGhTY2FsZWRUUE1fc3ViWywnU1JTMzkwNjA3J10pCmBgYApZZWFoLCBhIGZldy4gQSBwbGF0ZWF1IGF0IGEgbWVkaWFuIG9mIDUwLgpgYGB7ciwgZmlnLndpZHRoPTMsIGZpZy5oZWlnaHQ9Mn0KYXBwbHkobGVuZ3RoU2NhbGVkVFBNLDIsZnVuY3Rpb24oeCkgbWVkaWFuKHgpKSAlPiUgc29ydCgpICU+JSBoZWFkKDYwKSAlPiUgZGF0YS5mcmFtZSgpICU+JSBmb3JtYXR0YWJsZSgpCgphcHBseShsZW5ndGhTY2FsZWRUUE0sMixmdW5jdGlvbih4KSBtZWRpYW4oeCkpICU+JSBkZW5zaXR5KCkgJT4lIHBsb3QoKQpheGlzKHNpZGUgPSAxLCBhdD1zZXEoMCw1MDAsMTApKQpgYGAKCkxldCdzIHJlZG8gdGhlIHFzbW9vdGgnZWQgZGVuc2l0eSBwbG90cyBhYm92ZSwgYnV0IHdpdGggdGhlIGxvdyAoPDEwKSBjb2xvcmVkLiBCYW0sIHRoYXQncyBpdC4gCmBgYHtyLCBmaWcud2lkdGg9NiwgZmlnLmhlaWd0aD03fQptZWRpYW5fVFBNIDwtIGFwcGx5KGxlbmd0aFNjYWxlZFRQTSwyLGZ1bmN0aW9uKHgpIG1lZGlhbih4KSkgJT4lIHNvcnQoKSAlPiUgZm9ybWF0dGFibGUoKQpsb3dzIDwtIG5hbWVzKG1lZGlhbl9UUE1bbWVkaWFuX1RQTTwxMF0pCgpjb3JlX3RpZ2h0MiA8LSBjb3JlX3RpZ2h0CmNvcmVfdGlnaHQyJHNhbXBsZV9hY2Nlc3Npb24gPC0gZ3N1YihwYXR0ZXJuPSdFLU1UQUItJyxyZXBsYWNlbWVudCA9ICdFLk1UQUIuJyxjb3JlX3RpZ2h0JHNhbXBsZV9hY2Nlc3Npb24pCmdhdGhlcl9sU1Q8LWdhdGhlcihkYXRhLmZyYW1lKGxlbmd0aFNjYWxlZFRQTV9xc21vb3RoKSxzYW1wbGVfYWNjZXNzaW9uKSAlPiUgbGVmdF9qb2luKC4sY29yZV90aWdodDIpICU+JSBtdXRhdGUoTG93TWVkaWFuPWlmZWxzZShzYW1wbGVfYWNjZXNzaW9uICVpbiUgbG93cywgJ0xvd01lZGlhbicsJ09LJykpCmdncGxvdChnYXRoZXJfbFNULGFlcyh4PWxvZzIodmFsdWUrMSksZ3JvdXA9c2FtcGxlX2FjY2Vzc2lvbixjb2xvdXI9TG93TWVkaWFuKSkrZ2VvbV9kZW5zaXR5KGFscGhhPTAuNSkrZmFjZXRfd3JhcCh+VGlzc3VlKStjb29yZF9jYXJ0ZXNpYW4oeWxpbT1jKDAsMC41KSkrdGhlbWVfUHVibGljYXRpb24oKQpgYGAKCkxldCdzIHVwIHRoZSBtZWRpYW4gdGhyZXNob2xkIHRvLCBzYXksIDUwLiBUaGlzIGlzIHByb2JhYmx5IGEgcmVhc29uYWJsZSBwbGFjZSB0byBzZXQgaXQuIApgYGB7ciwgZmlnLndpZHRoPTYsIGZpZy5oZWlndGg9N30KbWVkaWFuX1RQTSA8LSBhcHBseShsZW5ndGhTY2FsZWRUUE0sMixmdW5jdGlvbih4KSBtZWRpYW4oeCkpICU+JSBzb3J0KCkgJT4lIGZvcm1hdHRhYmxlKCkKbG93cyA8LSBuYW1lcyhtZWRpYW5fVFBNW21lZGlhbl9UUE08NjBdKQoKY29yZV90aWdodDIgPC0gY29yZV90aWdodApjb3JlX3RpZ2h0MiRzYW1wbGVfYWNjZXNzaW9uIDwtIGdzdWIocGF0dGVybj0nRS1NVEFCLScscmVwbGFjZW1lbnQgPSAnRS5NVEFCLicsY29yZV90aWdodCRzYW1wbGVfYWNjZXNzaW9uKQpnYXRoZXJfbFNUPC1nYXRoZXIoZGF0YS5mcmFtZShsZW5ndGhTY2FsZWRUUE1fcXNtb290aCksc2FtcGxlX2FjY2Vzc2lvbikgJT4lIGxlZnRfam9pbiguLGNvcmVfdGlnaHQyKSAlPiUgbXV0YXRlKExvd01lZGlhbj1pZmVsc2Uoc2FtcGxlX2FjY2Vzc2lvbiAlaW4lIGxvd3MsICdMb3dNZWRpYW4nLCdPSycpKQpnZ3Bsb3QoZ2F0aGVyX2xTVCxhZXMoeD1sb2cyKHZhbHVlKzEpLGdyb3VwPXNhbXBsZV9hY2Nlc3Npb24sY29sb3VyPUxvd01lZGlhbikpK2dlb21fZGVuc2l0eShhbHBoYT0wLjUpK2ZhY2V0X3dyYXAoflRpc3N1ZSkrY29vcmRfY2FydGVzaWFuKHlsaW09YygwLDAuNSkpK3RoZW1lX1B1YmxpY2F0aW9uKCkKCgpgYGAKClVnaCwgdGhpcyB0b3NzZXMgYSBMT1Qgb2YgbXkgZmV0YWwgUlBFICg0NCBvcmlnaW5hbGx5KS4gRWgsIGxpZmUuIApgYGB7cn0KZ2F0aGVyX2xTVCAlPiUgZmlsdGVyKHNhbXBsZV9hY2Nlc3Npb24gJWluJSBsb3dzKSAlPiUgc2VsZWN0KHNhbXBsZV9hY2Nlc3Npb24sIFRpc3N1ZSwgU3ViX1Rpc3N1ZSkgJT4lIGRpc3RpbmN0KCkgJT4lIGdyb3VwX2J5KFRpc3N1ZSwgU3ViX1Rpc3N1ZSkgJT4lIHN1bW1hcmlzZShDb3VudD1uKCkpICU+JSBmb3JtYXR0YWJsZSgpCmBgYApSZWRvIHRoZSB0c25lIChxc21vb3RoJ2VkKSBhbmQgdG9zc2luZyB0aGUgYWJvdmUgc2FtcGxlcy4gTG9va3MgYmV0dGVyLiBBIGNvdXBsZSBvZiBSZXRpbmEgKCQpIHNhbXBsZXMgdGhhdCBoYXZlIHdhbmRlcmVkIGF3YXkgKFgyPTEwLFgxPS0xMCkuIApgYGB7ciwgZmlnLndpZHRoPTQsIGZpZy5oZWlndGg9NC41fQpzZXQuc2VlZCgyMzIzNSkKbGVuZ3RoU2NhbGVkVFBNX3FzbW9vdGhfaGlnaEV4cCA8LSBhcy5tYXRyaXgobGVuZ3RoU2NhbGVkVFBNX3FzbW9vdGhbLCFjb2xuYW1lcyhsZW5ndGhTY2FsZWRUUE1fcXNtb290aCkgJWluJSBsb3dzXSkKdHNuZV9vdXQgPC0gUnRzbmUoYXMubWF0cml4KGxvZzIodChsZW5ndGhTY2FsZWRUUE1fcXNtb290aF9oaWdoRXhwKSsxKSkscGVycGxleGl0eSA9IDUwLCBjaGVja19kdXBsaWNhdGVzID0gRkFMU0UsIHRoZXRhPTAuMCApCnRzbmVfcGxvdCA8LSBkYXRhLmZyYW1lKHRzbmVfb3V0JFkpCnRzbmVfcGxvdCRzYW1wbGVfYWNjZXNzaW9uIDwtIGNvbG5hbWVzKGxlbmd0aFNjYWxlZFRQTV9xc21vb3RoX2hpZ2hFeHApCgp0c25lX3Bsb3QgJT4lIGxlZnRfam9pbiguLGNvcmVfaW5mbykgICU+JQogIGdncGxvdCguLGFlcyh4PVgxLHk9WDIsY29sb3VyPVRpc3N1ZSxzaGFwZT1UaXNzdWUpKSArIAogIGdlb21fcG9pbnQoc2l6ZT00KSArIHNjYWxlX3NoYXBlX21hbnVhbCh2YWx1ZXM9YygwOjIwLDM1OjUwKSkgKwogIGdndGl0bGUocGFzdGUwKCJ0LXNuZS4gUGVycGxleGl0eSA9ICIsIDUwKSkgKwogIHRoZW1lX1B1YmxpY2F0aW9uKCkKYGBgCgpMZXQncyBkbyBhIHF1aWNrIGNoZWNrIG9uIGdlbmVzIHdpdGggb3ZlcmFsbCBsb3cgZXhwZXJlc3Npb24uIExlc3MgdGhhbiAxIGNvdW50IG9uIGF2ZXJhZ2UgYWNyb3NzIGFsbCBzYW1wbGVzLCBmb3IgYSBnZW5lLiAKYGBge3IsIGZpZy53aWR0aD0zLCBmaWcuaGVpZ3RoPTJ9CmRlbnNpdHkocm93U3Vtcyhsb2cyKGxlbmd0aFNjYWxlZFRQTSsxKSkpICU+JSBwbG90KCkKdGFibGUoKHJvd1N1bXMobGVuZ3RoU2NhbGVkVFBNKS9uY29sKGxlbmd0aFNjYWxlZFRQTSkpPDEpCmxvd2x5X2V4cHJlc3NlZF9nZW5lcyA8LSByb3cubmFtZXMobGVuZ3RoU2NhbGVkVFBNWyhyb3dTdW1zKGxlbmd0aFNjYWxlZFRQTSkvbmNvbChsZW5ndGhTY2FsZWRUUE0pPDEpLF0pCmBgYAoKT0ssIGxldCdzIGFsc28gdG9zcyB0aGVzZSBhbmQgc2VlIGhvdyB0aGF0IGNoYW5nZXMgdGhlIHQtc25lIGNsdXN0ZXJpbmcuIAoKYGBge3IsIGZpZy53aWR0aD00LCBmaWcuaGVpZ2h0PTQuNX0Kc2V0LnNlZWQoMjMyMzUpCmxlbmd0aFNjYWxlZFRQTV9xc21vb3RoX2hpZ2hFeHAgPC0gYXMubWF0cml4KGxlbmd0aFNjYWxlZFRQTV9xc21vb3RoWywhY29sbmFtZXMobGVuZ3RoU2NhbGVkVFBNX3FzbW9vdGgpICVpbiUgbG93c10pCmxlbmd0aFNjYWxlZFRQTV9xc21vb3RoX2hpZ2hFeHBfcmVtb3ZlX2xvd0dlbmVzIDwtIGxlbmd0aFNjYWxlZFRQTV9xc21vb3RoX2hpZ2hFeHBbIShyb3cubmFtZXMobGVuZ3RoU2NhbGVkVFBNX3FzbW9vdGhfaGlnaEV4cCkgJWluJSBsb3dseV9leHByZXNzZWRfZ2VuZXMpLF0KdHNuZV9vdXQgPC0gUnRzbmUoYXMubWF0cml4KGxvZzIodChsZW5ndGhTY2FsZWRUUE1fcXNtb290aF9oaWdoRXhwX3JlbW92ZV9sb3dHZW5lcykrMSkpLHBlcnBsZXhpdHkgPSA1MCwgY2hlY2tfZHVwbGljYXRlcyA9IEZBTFNFLCB0aGV0YT0wLjAgKQp0c25lX3Bsb3QgPC0gZGF0YS5mcmFtZSh0c25lX291dCRZKQp0c25lX3Bsb3Qkc2FtcGxlX2FjY2Vzc2lvbiA8LSBjb2xuYW1lcyhsZW5ndGhTY2FsZWRUUE1fcXNtb290aF9oaWdoRXhwX3JlbW92ZV9sb3dHZW5lcykKCnRzbmVfcGxvdCAlPiUgbGVmdF9qb2luKC4sY29yZV9pbmZvKSAgJT4lCiAgZ2dwbG90KC4sYWVzKHg9WDEseT1YMixjb2xvdXI9VGlzc3VlLHNoYXBlPVRpc3N1ZSkpICsgCiAgZ2VvbV9wb2ludChzaXplPTQpICsgc2NhbGVfc2hhcGVfbWFudWFsKHZhbHVlcz1jKDA6MjAsMzU6NTApKSArCiAgZ2d0aXRsZShwYXN0ZTAoInQtc25lLiBQZXJwbGV4aXR5ID0gIiwgNTApKSArCiAgdGhlbWVfUHVibGljYXRpb24oKQpgYGAKCk9LLCB0aGlzIGlzIGdldHRpbmcgaGFyZC9pbXBvc3NpYmxlIHRvIHNlZSB3aGV0aGVyIEknbSBkb2luZyBhbnkgYmV0dGVyLiBOZWVkIHRvIGdldCBtb3JlIGFuYWx5dGljYWwuIE1ldHJpYzogY2x1c3RlciBwdXJpdHkuIEstbWVhbnMgY2x1c3RlciAoayBudW1iZXIgaXMgbnVtYmVyIG9mIHN1Yi10aXNzdWVzIG9yIHRpc3N1ZXM/KSwgY2hlY2sgcHVyaXR5IG9mIGVhY2ggY2x1c3Rlci4KYGBge3J9CmxvYWQoJ34vZ2l0L3VuaWZpZWRfZ2VuZV9leHByZXNzaW9uL2RhdGEvbGVuZ3RoU2NhbGVkVFBNX2V5ZV9ndGV4LlJkYXRhJykKbGVuZ3RoU2NhbGVkVFBNIDwtIGxlbmd0aFNjYWxlZFRQTVssIShpcy5uYShsZW5ndGhTY2FsZWRUUE1bMSxdKSldCnN1YiA8LSB0aXNzdWVzICU+JSBmaWx0ZXIoVGlzc3VlICVpbiUga2VlcGVycykKbGVuZ3RoU2NhbGVkVFBNX3N1YjwtbGVuZ3RoU2NhbGVkVFBNWyxzdWIkc2FtcGxlX2FjY2Vzc2lvbl0Kc2V0LnNlZWQoMjMyMzUpCnRzbmVfb3V0IDwtIFJ0c25lKGFzLm1hdHJpeChsb2cyKHQobGVuZ3RoU2NhbGVkVFBNX3N1YikrMSkpLHBlcnBsZXhpdHkgPSA0MCwgY2hlY2tfZHVwbGljYXRlcyA9IEZBTFNFLCB0aGV0YT0wLjAgKQp0c25lX3Bsb3QgPC0gZGF0YS5mcmFtZSh0c25lX291dCRZKQp0c25lX3Bsb3Qkc2FtcGxlX2FjY2Vzc2lvbiA8LSBjb2xuYW1lcyhsZW5ndGhTY2FsZWRUUE1fc3ViKQoKdHNuZV9nbyA8LSB0c25lX3Bsb3QgJT4lIGxlZnRfam9pbiguLGNvcmVfaW5mbykKIyBEZXRlcm1pbmUgbnVtYmVyIG9mIGNsdXN0ZXJzCndzcyA8LSAobnJvdyh0c25lX2dvWyxjKCdYMScsJ1gyJyldKS0xKSpzdW0oYXBwbHkodHNuZV9nb1ssYygnWDEnLCdYMicpXSwyLHZhcikpCmZvciAoaSBpbiAyOjYwKSB3c3NbaV0gPC0gc3VtKGttZWFucyh0c25lX2dvWyxjKCdYMScsJ1gyJyldLCAKICAJY2VudGVycz1pKSR3aXRoaW5zcykKcGxvdCgxOjYwLCB3c3MsIHR5cGU9ImIiLCB4bGFiPSJOdW1iZXIgb2YgQ2x1c3RlcnMiLAogIHlsYWI9IldpdGhpbiBncm91cHMgc3VtIG9mIHNxdWFyZXMiKQojIDI0IGlzIHRoZSB0b3RhbCBudW1iZXIgb2YgVGlzc3VlcwojIDQ4IGlzIHRoZSB0b3RhbCBudW1iZXIgb2YgU3ViIFRpc3N1ZXMKc2V0LnNlZWQoMjMyMzUpCmZpdCA8LSBrbWVhbnModHNuZV9nb1ssYygnWDEnLCdYMicpXSwgMjQpCmNiaW5kKHRzbmVfZ28sIGZpdCRjbHVzdGVyKSAlPiUgZ3JvdXBfYnkoZml0JGNsdXN0ZXIsVGlzc3VlKSAlPiUgc3VtbWFyaXNlKENvdW50ID0gbigpKSAlPiUgbXV0YXRlKGZyZXEgPSBDb3VudCAvc3VtKENvdW50KSkgJT4lIGZvcm1hdHRhYmxlKCkKYGBgCgpOb3cgZ28gZGVlcGVyIGFuZCBjb3VudCBudW1iZXIgb2YgVGlzc3VlIHR5cGVzIGFuZCBwdXJpdHkgKGhpZ2hlc3QgZnJlcSkgZm9yIGVhY2ggY2x1c3RlcgpgYGB7cn0KY2JpbmQodHNuZV9nbywgZml0JGNsdXN0ZXIpICU+JSBncm91cF9ieShmaXQkY2x1c3RlcixUaXNzdWUpICU+JSBzdW1tYXJpc2UoQ291bnQgPSBuKCkpICU+JSBtdXRhdGUoZnJlcSA9IENvdW50IC9zdW0oQ291bnQpKSAlPiUgc3VtbWFyaXNlKFRpc3N1ZV9Db3VudCA9IG4oKSwgaGlnaGVzdF9mcmVxPW1heChmcmVxKSkgJT4lIGZvcm1hdHRhYmxlKCkgCmBgYApPSywgbm93IHRha2UgdGhlIG1lYW4gb2YgVGlzc3VlX0NvdW50IGFuZCBoaWdoZXN0X2ZyZXEsIGZvciB0d28gbWV0cmljcyBmb3IgZWFjaCB0bnNlLgpgYGB7cn0KY2JpbmQodHNuZV9nbywgZml0JGNsdXN0ZXIpICU+JSBncm91cF9ieShmaXQkY2x1c3RlcixUaXNzdWUpICU+JSBzdW1tYXJpc2UoQ291bnQgPSBuKCkpICU+JSBtdXRhdGUoZnJlcSA9IENvdW50IC9zdW0oQ291bnQpKSAlPiUgc3VtbWFyaXNlKFRpc3N1ZV9Db3VudCA9IG4oKSwgaGlnaGVzdF9mcmVxPW1heChmcmVxKSkgJT4lIG11dGF0ZShtZWFuKFRpc3N1ZV9Db3VudCksIG1lYW4oaGlnaGVzdF9mcmVxKSkgJT4lIHNlbGVjdChgbWVhbihUaXNzdWVfQ291bnQpYCwgYG1lYW4oaGlnaGVzdF9mcmVxKWApICU+JSBkaXN0aW5jdCgpCmBgYApDb29sLiBMZXQncyBnbyB0aGUgZW5kIChxc21vb3RoLCByZW1vdmFsIG9mIGxvdy1tZWRpYW4gc2FtcGxlcywgcmVtb3ZhbCBvZiBsb3cgZXhwcmVzc2lvbiBnZW5lcykgYW5kIHNlZSBpZiB0aGVzZSBtZXRyaWNzIGdldCBiZXR0ZXIuIFlFUyBUSEVZIERPLiBJVCBXT1JLUy4gT01HLiAoU29ycnksIHVzdWFsbHkgc3R1ZmYgbGlrZSB0aGlzIGJsb3dzIHVwIGluIG15IGZhY2Ugb25jZSBJIHRlc3QgaXQgcmlnb3JvdXNseSkuIApgYGB7cn0KCnNldC5zZWVkKDIzMjM1KQp0c25lX291dCA8LSBSdHNuZShhcy5tYXRyaXgobG9nMih0KGxlbmd0aFNjYWxlZFRQTV9xc21vb3RoX2hpZ2hFeHBfcmVtb3ZlX2xvd0dlbmVzKSsxKSkscGVycGxleGl0eSA9IDQwLCBjaGVja19kdXBsaWNhdGVzID0gRkFMU0UsIHRoZXRhPTAuMCApCnRzbmVfcGxvdCA8LSBkYXRhLmZyYW1lKHRzbmVfb3V0JFkpCnRzbmVfcGxvdCRzYW1wbGVfYWNjZXNzaW9uIDwtIGNvbG5hbWVzKGxlbmd0aFNjYWxlZFRQTV9xc21vb3RoX2hpZ2hFeHBfcmVtb3ZlX2xvd0dlbmVzKQoKdHNuZV9nbyA8LSB0c25lX3Bsb3QgJT4lIGxlZnRfam9pbiguLGNvcmVfaW5mbykKCnNldC5zZWVkKDIzMjM1KQpmaXQgPC0ga21lYW5zKHRzbmVfZ29bLGMoJ1gxJywnWDInKV0sIDI0KQpjYmluZCh0c25lX2dvLCBmaXQkY2x1c3RlcikgJT4lIGdyb3VwX2J5KGZpdCRjbHVzdGVyLFRpc3N1ZSkgJT4lIHN1bW1hcmlzZShDb3VudCA9IG4oKSkgJT4lIG11dGF0ZShmcmVxID0gQ291bnQgL3N1bShDb3VudCkpICU+JSBzdW1tYXJpc2UoVGlzc3VlX0NvdW50ID0gbigpLCBoaWdoZXN0X2ZyZXE9bWF4KGZyZXEpKSAlPiUgbXV0YXRlKG1lYW4oVGlzc3VlX0NvdW50KSwgbWVhbihoaWdoZXN0X2ZyZXEpKSAlPiUgc2VsZWN0KGBtZWFuKFRpc3N1ZV9Db3VudClgLCBgbWVhbihoaWdoZXN0X2ZyZXEpYCkgJT4lIGRpc3RpbmN0KCkKYGBgCgpMZXQncyBzZWUgd2hldGhlciBoYXZpbmcgcXNtb290aCBjb3JyZWN0IGZvciBTdWIgVGlzc3VlcyBpbXByb3ZlcyBwZXJmb3JtYW5jZS4KCmBgYHtyfQpsb2FkKCd+L2dpdC91bmlmaWVkX2dlbmVfZXhwcmVzc2lvbi9kYXRhL2xlbmd0aFNjYWxlZFRQTV9leWVfZ3RleC5SZGF0YScpCmxlbmd0aFNjYWxlZFRQTSA8LSBsZW5ndGhTY2FsZWRUUE1bLCEoaXMubmEobGVuZ3RoU2NhbGVkVFBNWzEsXSkpXQoKCnNhbXBsZXMgPC0gZGF0YS5mcmFtZShjb2xuYW1lcyhsZW5ndGhTY2FsZWRUUE0pKQpjb2xuYW1lcyhzYW1wbGVzKSA8LSAnc2FtcGxlX2FjY2Vzc2lvbicKdGlzc3VlX2ZyYW1lIDwtIGxlZnRfam9pbihzYW1wbGVzLCBjb3JlX2luZm8pICU+JSBzZWxlY3Qoc2FtcGxlX2FjY2Vzc2lvbiwgVGlzc3VlLCBTdWJfVGlzc3VlKSAlPiUgZGlzdGluY3QoKQojIG1ha2Ugc3VyZSB0aGV5IGFyZSBsaW5lZCB1cAp0aXNzdWVzIDwtIGxlZnRfam9pbihzYW1wbGVzLCB0aXNzdWVfZnJhbWUpCiMganVzdCBrZWVwIHRoZSBrZWVwZXIgc2V0IG9mIHRpc3N1ZXMgKG5vIHByb3N0cmF0ZSwgZXRjKQpzdWIgPC0gdGlzc3VlcyAlPiUgZmlsdGVyKFRpc3N1ZSAlaW4lIGtlZXBlcnMpCmxlbmd0aFNjYWxlZFRQTV9zdWI8LWxlbmd0aFNjYWxlZFRQTVssc3ViJHNhbXBsZV9hY2Nlc3Npb25dCnFzIDwtIHFzbW9vdGgob2JqZWN0ID0gbGVuZ3RoU2NhbGVkVFBNX3N1Yixncm91cEZhY3RvciA9IGFzLmZhY3RvcihzdWIkU3ViX1Rpc3N1ZSkpCmxlbmd0aFNjYWxlZFRQTV9xc21vb3RoX3N1YlRpc3N1ZSA8LSBxc21vb3RoRGF0YShxcykKCmxlbmd0aFNjYWxlZFRQTV9xc21vb3RoX2hpZ2hFeHBfc3ViVGlzc3VlIDwtIGFzLm1hdHJpeChsZW5ndGhTY2FsZWRUUE1fcXNtb290aF9zdWJUaXNzdWVbLCFjb2xuYW1lcyhsZW5ndGhTY2FsZWRUUE1fcXNtb290aF9zdWJUaXNzdWUpICVpbiUgbG93c10pCmxlbmd0aFNjYWxlZFRQTV9xc21vb3RoX2hpZ2hFeHBfcmVtb3ZlX2xvd0dlbmVzX3N1YlRpc3N1ZSA8LSBsZW5ndGhTY2FsZWRUUE1fcXNtb290aF9oaWdoRXhwX3N1YlRpc3N1ZVshKHJvdy5uYW1lcyhsZW5ndGhTY2FsZWRUUE1fcXNtb290aF9oaWdoRXhwX3N1YlRpc3N1ZSkgJWluJSBsb3dseV9leHByZXNzZWRfZ2VuZXMpLF0KCiMjIyBub3QgJ2NvcnJlY3RlZCcKc2V0LnNlZWQoMjMyMzUpCnRzbmVfb3V0IDwtIFJ0c25lKGFzLm1hdHJpeChsb2cyKHQobGVuZ3RoU2NhbGVkVFBNX3FzbW9vdGhfc3ViVGlzc3VlKSsxKSkscGVycGxleGl0eSA9IDQwLCBjaGVja19kdXBsaWNhdGVzID0gRkFMU0UsIHRoZXRhPTAuMCApCnRzbmVfcGxvdCA8LSBkYXRhLmZyYW1lKHRzbmVfb3V0JFkpCnRzbmVfcGxvdCRzYW1wbGVfYWNjZXNzaW9uIDwtIGNvbG5hbWVzKGxlbmd0aFNjYWxlZFRQTV9xc21vb3RoX3N1YlRpc3N1ZSkKCnRzbmVfZ28gPC0gdHNuZV9wbG90ICU+JSBsZWZ0X2pvaW4oLixjb3JlX2luZm8pCgpzZXQuc2VlZCgyMzIzNSkKZml0IDwtIGttZWFucyh0c25lX2dvWyxjKCdYMScsJ1gyJyldLCAyNCkKY2JpbmQodHNuZV9nbywgZml0JGNsdXN0ZXIpICU+JSBncm91cF9ieShmaXQkY2x1c3RlcixUaXNzdWUpICU+JSBzdW1tYXJpc2UoQ291bnQgPSBuKCkpICU+JSBtdXRhdGUoZnJlcSA9IENvdW50IC9zdW0oQ291bnQpKSAlPiUgc3VtbWFyaXNlKFRpc3N1ZV9Db3VudCA9IG4oKSwgaGlnaGVzdF9mcmVxPW1heChmcmVxKSkgJT4lIG11dGF0ZShtZWFuKFRpc3N1ZV9Db3VudCksIG1lYW4oaGlnaGVzdF9mcmVxKSkgJT4lIHNlbGVjdChgbWVhbihUaXNzdWVfQ291bnQpYCwgYG1lYW4oaGlnaGVzdF9mcmVxKWApICU+JSBkaXN0aW5jdCgpCgoKYGBgCk5vdyB3aXRoIHRoZSBjb3JyZWN0aW9ucy4uLi5raW5kIG9mIG1pZGRsaW5nLiBNYXkgbm90IGJlIGVub3VnaCB0aXNzdWVzIHBlciBncm91cC4gCmBgYHtyfQpzZXQuc2VlZCgyMzIzNSkKdHNuZV9vdXQgPC0gUnRzbmUoYXMubWF0cml4KGxvZzIodChsZW5ndGhTY2FsZWRUUE1fcXNtb290aF9oaWdoRXhwX3JlbW92ZV9sb3dHZW5lc19zdWJUaXNzdWUpKzEpKSxwZXJwbGV4aXR5ID0gNDAsIGNoZWNrX2R1cGxpY2F0ZXMgPSBGQUxTRSwgdGhldGE9MC4wICkKdHNuZV9wbG90IDwtIGRhdGEuZnJhbWUodHNuZV9vdXQkWSkKdHNuZV9wbG90JHNhbXBsZV9hY2Nlc3Npb24gPC0gY29sbmFtZXMobGVuZ3RoU2NhbGVkVFBNX3FzbW9vdGhfaGlnaEV4cF9yZW1vdmVfbG93R2VuZXNfc3ViVGlzc3VlKQoKdHNuZV9nbyA8LSB0c25lX3Bsb3QgJT4lIGxlZnRfam9pbiguLGNvcmVfaW5mbykKCnNldC5zZWVkKDIzMjM1KQpmaXQgPC0ga21lYW5zKHRzbmVfZ29bLGMoJ1gxJywnWDInKV0sIDI0KQpjYmluZCh0c25lX2dvLCBmaXQkY2x1c3RlcikgJT4lIGdyb3VwX2J5KGZpdCRjbHVzdGVyLFRpc3N1ZSkgJT4lIHN1bW1hcmlzZShDb3VudCA9IG4oKSkgJT4lIG11dGF0ZShmcmVxID0gQ291bnQgL3N1bShDb3VudCkpICU+JSBzdW1tYXJpc2UoVGlzc3VlX0NvdW50ID0gbigpLCBoaWdoZXN0X2ZyZXE9bWF4KGZyZXEpKSAlPiUgbXV0YXRlKG1lYW4oVGlzc3VlX0NvdW50KSwgbWVhbihoaWdoZXN0X2ZyZXEpKSAlPiUgc2VsZWN0KGBtZWFuKFRpc3N1ZV9Db3VudClgLCBgbWVhbihoaWdoZXN0X2ZyZXEpYCkgJT4lIGRpc3RpbmN0KCkKYGBgCgpPSywgc2F2aW5nIGxlbmd0aFNjYWxlZFRQTV9xc21vb3RoX2hpZ2hFeHBfcmVtb3ZlX2xvd0dlbmVzIGZvciBkb3duc3RyZWFtIHVzZToKYGBge3J9CnNhdmUobGVuZ3RoU2NhbGVkVFBNX3FzbW9vdGhfaGlnaEV4cF9yZW1vdmVfbG93R2VuZXMsIGZpbGU9J34vZ2l0L3VuaWZpZWRfZ2VuZV9leHByZXNzaW9uL2RhdGEvbGVuZ3RoU2NhbGVkVFBNX3Byb2Nlc3NlZC5SZGF0YScpCmBgYAoK